// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
//
// This file generates the extension methods public API and the extension
// methods patch file for all integers, double, and float.
// The PointerPointer and PointerStruct extension are written by hand since
// those are not repetitive.

import 'dart:io';

import 'package:args/args.dart';

//
// Configuration.
//

const configuration = [
  Config("Int8", "int", "Int8List", 1),
  Config("Int16", "int", "Int16List", 2),
  Config("Int32", "int", "Int32List", 4),
  Config("Int64", "int", "Int64List", 8),
  Config("Uint8", "int", "Uint8List", 1),
  Config("Uint16", "int", "Uint16List", 2),
  Config("Uint32", "int", "Uint32List", 4),
  Config("Uint64", "int", "Uint64List", 8),
  Config("IntPtr", "int", kDoNotEmit, kIntPtrElementSize),
  Config("Float", "double", "Float32List", 4),
  Config("Double", "double", "Float64List", 8),
];

//
// Generator.
//

main(List<String> arguments) {
  final args = argParser().parse(arguments);
  Uri path = Uri.parse(args['path']);

  generate(path, "ffi.g.dart", generatePublicExtension);
  generate(path, "ffi_patch.g.dart", generatePatchExtension);
}

void generate(
    Uri path, String fileName, Function(StringBuffer, Config) generator) {
  final buffer = StringBuffer();
  generateHeader(buffer);
  configuration.forEach((Config c) => generator(buffer, c));
  generateFooter(buffer);

  final fullPath = path.resolve(fileName).path;
  File(fullPath).writeAsStringSync(buffer.toString());
  final fmtResult = Process.runSync(dartfmtPath().path, ["-w", fullPath]);
  if (fmtResult.exitCode != 0) {
    throw Exception(
        "Formatting failed:\n${fmtResult.stdout}\n${fmtResult.stderr}\n");
  }
  print("Generated $fullPath.");
}

void generateHeader(StringBuffer buffer) {
  const header = """
//
// The following code is generated, do not edit by hand.
//
// Code generated by `runtime/tools/ffi/sdk_lib_ffi_generator.dart`.
//

""";

  buffer.write(header);
}

void generatePublicExtension(StringBuffer buffer, Config config) {
  final nativeType = config.nativeType;
  final dartType = config.dartType;
  final typedListType = config.typedListType;
  final elementSize = config.elementSize;

  final bits = sizeOfBits(elementSize);
  // final sizeInBytes =
  // "${sizeOf(elementSize)} byte${elementSize != 1 ? "s" : ""}";

  String property;
  if (_isInt(nativeType)) {
    if (_isSigned(nativeType)) {
      property = "$bits-bit two's complement integer";
    } else {
      property = "$bits-bit unsigned integer";
    }
  } else if (nativeType == "Float") {
    property = "float";
  } else {
    property = "double";
  }

  const platformIntPtr = """
  ///
  /// On 32-bit platforms this is a 32-bit integer, and on 64-bit platforms
  /// this is a 64-bit integer.
""";

  final platform = nativeType == "IntPtr" ? platformIntPtr : "";

  final intSignedTruncate = """
  ///
  /// A Dart integer is truncated to $bits bits (as if by `.toSigned($bits)`) before
  /// being stored, and the $bits-bit value is sign-extended when it is loaded.
""";

  const intPtrTruncate = """
  ///
  /// On 32-bit platforms a Dart integer is truncated to 32 bits (as if by
  /// `.toSigned(32)`) before being stored, and the 32-bit value is
  /// sign-extended when it is loaded.
""";

  final intUnsignedTruncate = """
  ///
  /// A Dart integer is truncated to $bits bits (as if by `.toUnsigned($bits)`) before
  /// being stored, and the $bits-bit value is zero-extended when it is loaded.
""";

  const floatTruncate = """
  ///
  /// A Dart double loses precision before being stored, and the float value is
  /// converted to a double when it is loaded.
""";

  String truncate = "";
  if (nativeType == "IntPtr") {
    truncate = intPtrTruncate;
  } else if (_isInt(nativeType) && elementSize != 8) {
    truncate = _isSigned(nativeType) ? intSignedTruncate : intUnsignedTruncate;
  } else if (nativeType == "Float") {
    truncate = floatTruncate;
  }

  final sizeTimes =
      elementSize != 1 ? '${bracketOr(sizeOf(elementSize))} * ' : '';

  final alignmentDefault = """
  ///
  /// The [address] must be ${sizeOf(elementSize)}-byte aligned.
""";

  const alignmentIntptr = """
  ///
  /// On 32-bit platforms the [address] must be 4-byte aligned, and on 64-bit
  /// platforms the [address] must be 8-byte aligned.
""";

  String alignment = "";
  if (nativeType == "IntPtr") {
    alignment = alignmentIntptr;
  } else if (elementSize != 1) {
    alignment = alignmentDefault;
  }

  final asTypedList = typedListType == kDoNotEmit
      ? ""
      : """
  /// Creates a typed list view backed by memory in the address space.
  ///
  /// The returned view will allow access to the memory range from [address]
  /// to `address + ${sizeTimes}length`.
  ///
  /// The user has to ensure the memory range is accessible while using the
  /// returned list.
$alignment  external $typedListType asTypedList(int length);
""";

  // TODO(38892): Stop generating documentation on setter.
  buffer.write("""
/// Extension on [Pointer] specialized for the type argument [$nativeType].
extension ${nativeType}Pointer on Pointer<$nativeType> {
  /// The $property at [address].
$platform$truncate$alignment  external $dartType get value;

  /// The $property at [address].
$platform$truncate$alignment  external void set value($dartType value);

  /// The $property at `address + ${sizeTimes}index`.
$platform$truncate$alignment  external $dartType operator [](int index);

  /// The $property at `address + ${sizeTimes}index`.
$platform$truncate$alignment  external void operator []=(int index, $dartType value);

$asTypedList
}

""");
}

void generatePatchExtension(StringBuffer buffer, Config config) {
  final nativeType = config.nativeType;
  final dartType = config.dartType;
  final typedListType = config.typedListType;

  final asTypedList = typedListType == kDoNotEmit
      ? ""
      : """
  @patch
  $typedListType asTypedList(int elements) => _asExternalTypedData(this, elements);
""";

  buffer.write("""
extension ${nativeType}Pointer on Pointer<$nativeType> {
 @patch
  $dartType get value => _load$nativeType(this, 0);

  @patch
  set value($dartType value) => _store$nativeType(this, 0, value);

  @patch
  $dartType operator [](int index) => _load$nativeType(this, index);

  @patch
  operator []=(int index, $dartType value) => _store$nativeType(this, index, value);

$asTypedList
}

""");
}

void generateFooter(StringBuffer buffer) {
  final footer = """
//
// End of generated code.
//
""";

  buffer.write(footer);
}

//
// Helper functions.
//

bool _isInt(String type) => type.startsWith("Int") || type.startsWith("Uint");
bool _isSigned(String type) => type.startsWith("Int");

String sizeOf(int size) {
  switch (size) {
    case kIntPtrElementSize:
      return "4 or 8";
    default:
      return "$size";
  }
}

String sizeOfBits(int size) {
  switch (size) {
    case kIntPtrElementSize:
      return "32 or 64";
    default:
      return "${size * 8}";
  }
}

String bracketOr(String input) {
  if (input.contains("or")) {
    return "($input)";
  }
  return input;
}

final Uri _containingFolder = File.fromUri(Platform.script).parent.uri;

ArgParser argParser() {
  final parser = ArgParser(allowTrailingOptions: false);
  parser.addOption('path',
      abbr: 'p',
      help: 'Path to generate the files at.',
      defaultsTo: _containingFolder.toString());
  parser.addFlag('help', abbr: 'h', help: 'Display usage information.',
      callback: (help) {
    if (help) print(parser.usage);
  });
  return parser;
}

Uri dartfmtPath() {
  // TODO(dacoharkes): Use "../../../tools/sdks/dart-sdk/bin/dartfmt" when the
  // pinned fully supports extension methods.
  return Uri.parse("dartfmt");
}

class Config {
  final String nativeType;
  final String dartType;
  final String typedListType;
  final int elementSize;
  const Config(
      this.nativeType, this.dartType, this.typedListType, this.elementSize);
}

const String kDoNotEmit = "donotemit";
const int kIntPtrElementSize = -1;
